# -*- coding: utf-8 -*-
import os
import sys
import asyncio
from typing import Optional
from subprocess import Popen, PIPE

from fastapi import FastAPI, BackgroundTasks, Query
from fastapi.responses import JSONResponse
from fastapi.staticfiles import StaticFiles
from fastapi.responses import HTMLResponse, PlainTextResponse, StreamingResponse
import time
from config.module_map import resolve_test_path_by_module
from config.config import Current_log_path, JSON_REPORT
import json
import glob
import uuid

# 切换到项目根，确保 pytest.ini、路径等生效
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
os.chdir(BASE_DIR)

app = FastAPI(title="HOK Interface AutoTest Runner", version="1.0.0")

# 挂载静态目录以便直接访问 Allure 报告
OUTPUT_DIR = os.path.join(BASE_DIR, "output")
if os.path.isdir(OUTPUT_DIR):
    app.mount("/output", StaticFiles(directory=OUTPUT_DIR), name="output")


def _run_cmd(cmd: list[str]) -> tuple[int, str, str]:
    """Run a shell command and return (exit_code, stdout, stderr)."""
    proc = Popen(cmd, cwd=BASE_DIR, stdout=PIPE, stderr=PIPE, text=True)
    stdout, stderr = proc.communicate()
    return proc.returncode, stdout or "", stderr or ""


def update_repo_from_gitee() -> None:
    """Ensure local repo is up-to-date with Gitee before running tests.

    Behavior:
    - If not a git repo, skip silently (assumes deployed as source package).
    - Ensure `origin` points to Gitee URL (overridable via env `GIT_REPO_URL`).
    - Fetch and pull latest changes using the current branch's upstream.
    """
    repo_url = os.environ.get("GIT_REPO_URL", "https://gitee.com/luohao98/auto-test.git")

    log_path = os.path.join(BASE_DIR, "current_log.log")

    def append_log(text: str) -> None:
        try:
            with open(log_path, "a", encoding="utf-8") as f:
                f.write(text)
        except Exception:
            pass

    if not os.path.isdir(os.path.join(BASE_DIR, ".git")):
        append_log("\n[repo-update] .git not found, skip pulling from Gitee.\n")
        return

    append_log("\n[repo-update] Start syncing from Gitee...\n")

    # Ensure origin exists and points to the desired URL
    code, out, err = _run_cmd(["git", "remote"])
    remotes = out.strip().split() if code == 0 else []
    if "origin" in remotes:
        _run_cmd(["git", "remote", "set-url", "origin", repo_url])
    else:
        _run_cmd(["git", "remote", "add", "origin", repo_url])

    # Fetch latest
    code, out, err = _run_cmd(["git", "fetch", "--prune", "origin"])
    append_log(out)
    if err:
        append_log("[repo-update][fetch][stderr]\n" + err + "\n")

    # Determine current branch
    code, branch, err = _run_cmd(["git", "rev-parse", "--abbrev-ref", "HEAD"])
    branch = branch.strip() if code == 0 else ""

    # Pull using upstream if configured; fallback to origin/<branch>
    pull_cmd = ["git", "pull", "--rebase", "--autostash"]
    if branch:
        pull_cmd += ["origin", branch]
    code, out, err = _run_cmd(pull_cmd)
    append_log(out)
    if err:
        append_log("[repo-update][pull][stderr]\n" + err + "\n")

    append_log("[repo-update] Done.\n")


def _run_lock_path(run_id: str) -> str:
    return os.path.join(BASE_DIR, "output", f"run_{run_id}.lock")


def run_all_run(scope: str = "web", test_path: Optional[str] = None, k_expr: Optional[str] = None, run_id: Optional[str] = None) -> int:
    """同步执行 all_run.py，返回退出码"""
    # Always try to update codebase from Gitee before running
    try:
        update_repo_from_gitee()
    except Exception:
        # Best-effort; continue even if update fails
        pass
    python_exec = sys.executable
    env = os.environ.copy()
    # 将 scope 传入为环境变量，便于后续扩展（如根据scope在all_run里决定main分支）
    env["HOK_RUN_SCOPE"] = scope
    if test_path:
        env["HOK_TEST_PATH"] = test_path
    if k_expr:
        env["HOK_K_EXPR"] = k_expr
    if run_id:
        env["HOK_RUN_ID"] = run_id
    # 创建运行锁文件，标记运行中
    if run_id:
        try:
            os.makedirs(os.path.join(BASE_DIR, "output"), exist_ok=True)
            with open(_run_lock_path(run_id), "w", encoding="utf-8") as f:
                f.write(str(int(time.time())))
        except Exception:
            pass
    proc = Popen([python_exec, "all_run.py"], cwd=BASE_DIR, stdout=PIPE, stderr=PIPE, text=True, env=env)
    stdout, stderr = proc.communicate()
    exit_code = proc.returncode
    log_path = os.path.join(BASE_DIR, "current_log.log")
    # 将本次运行的标准输出写入日志末尾，便于排查
    try:
        with open(log_path, "a", encoding="utf-8") as f:
            f.write("\n\n===== API Triggered Run (scope=%s) =====\n" % scope)
            f.write(stdout or "")
            if stderr:
                f.write("\n[stderr]\n" + stderr)
    except Exception:
        pass
    # 移除运行锁文件
    if run_id:
        try:
            os.remove(_run_lock_path(run_id))
        except Exception:
            pass
    return exit_code


async def run_all_run_async(scope: str = "web", test_path: Optional[str] = None, k_expr: Optional[str] = None, run_id: Optional[str] = None) -> int:
    loop = asyncio.get_event_loop()
    return await loop.run_in_executor(None, run_all_run, scope, test_path, k_expr, run_id)


def _infer_scope_by_path(path: Optional[str], fallback: str = "web") -> str:
    if not path:
        return fallback
    norm = path.replace("\\", "/").lstrip("./")
    if norm.startswith("test_case/test_management_app"):
        return "app"
    if norm.startswith("test_case/test_web"):
        return "web"
    if norm.startswith("test_case/test_user_termainal"):
        return "c"
    return fallback


@app.post("/run")
async def run(background_tasks: BackgroundTasks,
              scope: Optional[str] = Query(default="auto", regex="^(auto|web|app|c|all)$"),
              test_path: Optional[str] = Query(default=None),
              k: Optional[str] = Query(default=None),
              module: Optional[str] = Query(default=None),
              env: Optional[str] = Query(default=None, description="测试环境: TEST/BETA/SOA"),
              force_update: Optional[bool] = Query(default=False)):
    """后台触发一次测试运行。可选参数: scope、test_path、k(-k 关键字)"""
    # 根据 module 解析映射路径（优先级：module > test_path）
    mapped_path = resolve_test_path_by_module(module)
    final_test_path = mapped_path if mapped_path else test_path
    final_scope = scope
    if scope == "auto":
        # 若未提供路径且选择 auto，则执行全部用例
        final_scope = "all" if not final_test_path else _infer_scope_by_path(final_test_path, fallback="web")

    # 若显式要求，先同步从 Gitee 拉取
    git_updated = False
    if force_update:
        try:
            update_repo_from_gitee()
            git_updated = True
        except Exception:
            git_updated = False

    run_id = uuid.uuid4().hex[:12]
    # 传递环境变量给子进程，以覆盖 config.NOW_ENV
    if env:
        os.environ["HOK_NOW_ENV"] = (env or "BETA").upper()
    background_tasks.add_task(run_all_run_async, final_scope, final_test_path, k, run_id)
    # 提前返回任务已受理
    report_url = f"/output/html_report/index.html?t={int(time.time())}"
    log_url = f"/logs/current?t={int(time.time())}"
    stream_url = f"/logs/stream?t={int(time.time())}&from_begin=false"
    status_url = f"/status/{run_id}"
    return JSONResponse({
        "ok": True,
        "message": "task accepted",
        "scope": final_scope,
        "module": module,
        "test_path": final_test_path,
        "report_url": report_url,
        "log_url": log_url,
        "stream_url": stream_url,
        "run_id": run_id,
        "status_url": status_url,
        "git_updated": git_updated,
        "env": os.environ.get("HOK_NOW_ENV")
    })


@app.post("/run_sync")
async def run_sync(scope: Optional[str] = Query(default="auto", regex="^(auto|web|app|c|all)$"),
                   test_path: Optional[str] = Query(default=None),
                   k: Optional[str] = Query(default=None),
                   module: Optional[str] = Query(default=None),
                   env: Optional[str] = Query(default=None, description="测试环境: TEST/BETA/SOA"),
                   force_update: Optional[bool] = Query(default=False)):
    """同步执行测试，完成后返回执行结果与 Allure 报告地址。"""
    mapped_path = resolve_test_path_by_module(module)
    final_test_path = mapped_path if mapped_path else test_path
    final_scope = scope
    if scope == "auto":
        final_scope = "all" if not final_test_path else _infer_scope_by_path(final_test_path, fallback="web")
    # 若显式要求，先同步从 Gitee 拉取
    git_updated = False
    if force_update:
        try:
            update_repo_from_gitee()
            git_updated = True
        except Exception:
            git_updated = False

    run_id = uuid.uuid4().hex[:12]
    # 覆盖环境变量传入子进程
    if env:
        os.environ["HOK_NOW_ENV"] = (env or "BETA").upper()
    exit_code = await run_all_run_async(final_scope, final_test_path, k, run_id)
    success = (exit_code == 0)
    report_url = f"/output/html_report/index.html?t={int(time.time())}"
    log_url = f"/logs/current?t={int(time.time())}"
    stream_url = f"/logs/stream?t={int(time.time())}&from_begin=false"
    status_url = f"/status/{run_id}"
    return JSONResponse({
        "ok": True,
        "scope": final_scope,
        "module": module,
        "test_path": final_test_path,
        "k": k,
        "exit_code": exit_code,
        "success": success,
        "report_url": report_url,
        "log_url": log_url,
        "stream_url": stream_url,
        "run_id": run_id,
        "status_url": status_url,
        "git_updated": git_updated,
        "env": os.environ.get("HOK_NOW_ENV")
    })


@app.get("/health")
async def health():
    return {"status": "ok"}


# 本地启动: uvicorn server:app --host 0.0.0.0 --port 8000




@app.get("/debug", response_class=HTMLResponse)
async def debug_page():
    """提供一个简单的本地调试页面，用于便捷触发执行并查看返回。"""
    ts = int(time.time())
    return f"""
<!DOCTYPE html>
<html lang=\"zh-CN\">
<head>
  <meta charset=\"UTF-8\" />
  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />
  <title>HOK 本地调试</title>
  <style>
    body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial; padding: 24px; }}
    label {{ display: block; margin: 8px 0 4px; }}
    input, select {{ width: 100%; max-width: 520px; padding: 8px; }}
    .row {{ margin-bottom: 12px; }}
    button {{ margin-right: 8px; padding: 8px 12px; }}
    pre {{ background: #f6f8fa; padding: 12px; white-space: pre-wrap; }}
  </style>
  <script>
    async function callApi(syncMode) {{
      const scope = document.getElementById('scope').value;
      const env = document.getElementById('env') ? document.getElementById('env').value : '';
      const module = document.getElementById('module').value.trim();
      const test_path = document.getElementById('test_path').value.trim();
      const k = document.getElementById('k').value.trim();
      const params = new URLSearchParams();
      if (scope) params.append('scope', scope);
      if (env) params.append('env', env);
      if (module) params.append('module', module);
      if (test_path) params.append('test_path', test_path);
      if (k) params.append('k', k);
      const url = syncMode ? `/run_sync?${{params.toString()}}` : `/run?${{params.toString()}}`;
      const res = await fetch(url, {{ method: 'POST' }});
      const data = await res.json();
      document.getElementById('result').textContent = JSON.stringify(data, null, 2);
      if (data.report_url) {{
        const link = document.getElementById('report');
        link.href = data.report_url;
        link.textContent = data.report_url;
      }}
    }}
  </script>
</head>
<body>
  <h2>HOK 本地调试页面</h2>
  <div class=\"row\">
    <label for=\"scope\">scope (auto/web/app/c/all)</label>
    <select id=\"scope\">
      <option value=\"auto\" selected>auto（按路径自动判断）</option>
      <option value=\"web\">web</option>
      <option value=\"app\">app</option>
      <option value=\"c\">c</option>
      <option value=\"all\">all</option>
    </select>
  </div>
  <div class=\"row\">
    <label for=\"env\">env (TEST/BETA/SOA)</label>
    <select id=\"env\">
      <option value=\"TEST\">TEST</option>
      <option value=\"BETA\" selected>BETA</option>
      <option value=\"SOA\">SOA</option>
    </select>
  </div>
  <div class=\"row\">
    <label for=\"module\">module（优先级高于 test_path）</label>
    <input id=\"module\" placeholder=\"例如：直播模块 / 内容中心 / 师资中心 / 交易中心\" />
  </div>
  <div class=\"row\">
    <label for=\"test_path\">test_path（相对项目根，例如：test_case/test_web/test_live_management）</label>
    <input id=\"test_path\" />
  </div>
  <div class=\"row\">
    <label for=\"k\">-k 关键字（可选）</label>
    <input id=\"k\" placeholder=\"例如：test_live_console\" />
  </div>
  <div class=\"row\">
    <button onclick=\"callApi(true)\">同步执行（等待结果）</button>
    <button onclick=\"callApi(false)\">异步执行（立即返回）</button>
  </div>

  <h3>返回结果</h3>
  <pre id=\"result\"></pre>

  <h3>Allure 报告</h3>
  <a id=\"report\" href=\"/output/html_report/index.html?t={ts}\" target=\"_blank\">/output/html_report/index.html?t={ts}</a>

  <h3>实时日志</h3>
  <div>
    <div>轮询方式：<a href=\"/logs/current?t={ts}\" target=\"_blank\">/logs/current?t={ts}</a></div>
    <div>流式方式（SSE）：<a href=\"/logs/stream?t={ts}\" target=\"_blank\">/logs/stream?t={ts}</a></div>
  </div>

  <p style=\"margin-top:24px;color:#666\">提示：如果报告未生成，请稍等刷新或查看返回 JSON 的 report_url。</p>
</body>
</html>
    """


@app.get("/logs/current", response_class=PlainTextResponse)
async def current_log(n: Optional[int] = Query(default=500, ge=1, le=5000)):
    """返回当前执行日志的尾部 n 行文本，便于实时查看执行过程。"""
    try:
        with open(Current_log_path, "r", encoding="utf-8", errors="ignore") as f:
            lines = f.readlines()
        tail = lines[-n:] if n and len(lines) > n else lines
        # 禁用缓存
        headers = {"Cache-Control": "no-store"}
        return PlainTextResponse("".join(tail), headers=headers)
    except FileNotFoundError:
        return PlainTextResponse("当前无日志。", headers={"Cache-Control": "no-store"})


async def _tail_log_generator(from_begin: bool = False, interval: float = 1.0, timeout: int = 600):
    start_time = time.time()
    position = 0
    try:
        while True:
            if time.time() - start_time > timeout:
                yield b"event: end\n data: timeout\n\n"
                break
            try:
                with open(Current_log_path, "rb") as f:
                    if not from_begin and position == 0:
                        f.seek(0, 2)
                        position = f.tell()
                    else:
                        f.seek(position)
                    chunk = f.read()
                    if chunk:
                        position += len(chunk)
                        # 将块拆成行，逐行作为 SSE data 发送
                        for line in chunk.splitlines():
                            yield b"data: " + line.replace(b"\r", b"") + b"\n\n"
                    else:
                        await asyncio.sleep(interval)
                        continue
            except FileNotFoundError:
                await asyncio.sleep(interval)
                continue
    except asyncio.CancelledError:
        pass


@app.get("/logs/stream")
async def stream_log(from_begin: Optional[bool] = Query(default=False),
                    interval: Optional[float] = Query(default=1.0, ge=0.1, le=5.0),
                    timeout: Optional[int] = Query(default=600, ge=10, le=3600)):
    """基于 SSE 的实时日志输出。浏览器可直接打开，或用 curl 订阅。"""
    generator = _tail_log_generator(from_begin=from_begin, interval=interval, timeout=timeout)
    return StreamingResponse(generator, media_type="text/event-stream", headers={"Cache-Control": "no-store"})


def _collect_allure_statuses():
    results = []
    try:
        for path in glob.glob(os.path.join(JSON_REPORT, "*.json")):
            try:
                with open(path, "r", encoding="utf-8", errors="ignore") as f:
                    data = json.load(f)
                name = data.get("name") or data.get("fullName") or os.path.basename(path)
                status = data.get("status") or "unknown"
                if name:
                    results.append({"name": name, "status": status})
            except Exception:
                continue
    except Exception:
        pass
    return results


@app.get("/runs/{run_id}")
async def get_run_status(run_id: str):
    lock_exists = os.path.exists(os.path.join(BASE_DIR, "output", f"run_{run_id}.lock"))
    # 优先读取运行期状态文件
    status_file = os.path.join(BASE_DIR, "output", f"run_{run_id}_status.jsonl")
    tests = []
    if os.path.exists(status_file):
        try:
            with open(status_file, "r", encoding="utf-8", errors="ignore") as f:
                latest_by_name = {}
                order = []
                for line in f:
                    line = line.strip()
                    if not line:
                        continue
                    try:
                        obj = json.loads(line)
                        name = obj.get("name")
                        status = obj.get("status") or "unknown"
                        if not name:
                            continue
                        if name not in latest_by_name:
                            order.append(name)
                        latest_by_name[name] = status
                    except Exception:
                        continue
                # 输出去重后的用例，状态取最新一条
                tests = [{"name": n, "status": latest_by_name.get(n, "unknown")} for n in order]
        except Exception:
            pass
    # 仅展示我们实时记录的 YAML 用例名称与状态；不再回退显示 Allure 的容器文件名
    # 若没有实时状态（例如运行尚未开始），返回空列表，由前端通过全局 running 状态指示加载中
    status = "running" if lock_exists else "finished"
    # 统计进度
    counts = {"total": len(tests), "passed": 0, "failed": 0, "broken": 0, "skipped": 0, "running": 0}
    for t in tests:
        s = (t.get("status") or "").lower()
        if s in counts:
            counts[s] += 1
        elif s == "skipped":
            counts["skipped"] += 1
    counts["finished"] = counts["passed"] + counts["failed"] + counts["broken"] + counts["skipped"]
    counts["unfinished"] = max(counts["total"] - counts["finished"], 0)
    return {
        "run_id": run_id,
        "status": status,
        "tests": tests,
        "count": len(tests),
        "counts": counts
    }


@app.get("/status/{run_id}", response_class=HTMLResponse)
async def status_page(run_id: str):
    return f"""
<!DOCTYPE html>
<html lang=\"zh-CN\">
<head>
  <meta charset=\"UTF-8\" />
  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />
  <title>执行状态 - {run_id}</title>
  <style>
    body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial; padding: 20px; }}
    .row {{ margin-bottom: 10px; }}
    .badge {{ display: inline-block; padding: 2px 8px; border-radius: 10px; color: #fff; font-size: 12px; }}
    .running {{ background: #1677ff; }}
    .passed {{ background: #28a745; }}
    .failed {{ background: #dc3545; }}
    .broken {{ background: #fd7e14; }}
    .skipped {{ background: #6c757d; }}
    .unknown {{ background: #999; }}
    .spinner {{ display:inline-block; width:12px; height:12px; border:2px solid #ddd; border-top-color:#1677ff; border-radius:50%; animation:spin 0.8s linear infinite; margin-right:6px; }}
    @keyframes spin {{ to {{ transform: rotate(360deg); }} }}
    ul {{ padding-left: 18px; }}
    li {{ margin: 6px 0; }}
    a {{ color: #1677ff; text-decoration: none; }}
  </style>
  <script>
    async function fetchStatus() {{
      const res = await fetch('/runs/{run_id}');
      const data = await res.json();
      document.getElementById('run_id').textContent = data.run_id;
      const statusEl = document.getElementById('status');
      statusEl.textContent = data.status;
      statusEl.className = 'badge ' + (data.status === 'running' ? 'running' : 'passed');
      const globalSpinner = document.getElementById('global-spinner');
      if (data.status === 'running') {{
        globalSpinner.style.display = 'inline-block';
      }} else {{
        globalSpinner.style.display = 'none';
      }}
      // 顶部统计
      if (data.counts) {{
        const c = data.counts;
        document.getElementById('stat-total').textContent = c.total ?? 0;
        document.getElementById('stat-finished').textContent = c.finished ?? 0;
        document.getElementById('stat-passed').textContent = c.passed ?? 0;
        document.getElementById('stat-failed').textContent = c.failed ?? 0;
        document.getElementById('stat-broken').textContent = c.broken ?? 0;
        document.getElementById('stat-running').textContent = c.running ?? 0;
        document.getElementById('stat-unfinished').textContent = c.unfinished ?? 0;
      }}
      const list = document.getElementById('list');
      list.innerHTML = '';
      for (const t of data.tests) {{
        const li = document.createElement('li');
        const b = document.createElement('span');
        b.className = 'badge ' + (t.status || 'unknown');
        b.textContent = t.status || 'unknown';
        const name = document.createElement('span');
        name.textContent = ' ' + t.name;
        li.appendChild(b);
        if (data.status === 'running' && (!t.status || t.status === 'unknown')) {{
          const sp = document.createElement('span');
          sp.className = 'spinner';
          li.insertBefore(sp, b);
        }}
        li.appendChild(name);
        list.appendChild(li);
      }}
      if (data.status === 'running') {{
        setTimeout(fetchStatus, 2000);
      }}
    }}
    window.onload = fetchStatus;
  </script>
</head>
<body>
  <h3>执行状态</h3>
  <div class=\"row\">Run ID：<code id=\"run_id\"></code></div>
  <div class=\"row\">状态：<span id=\"status\" class=\"badge running\">running</span> <span id=\"global-spinner\" style=\"display:inline-block; width:12px; height:12px; border:2px solid #ddd; border-top-color:#1677ff; border-radius:50%; animation:spin 0.8s linear infinite; vertical-align: middle;\"></span></div>
  <div class=\"row\">
    进度：总 <b id=\"stat-total\">0</b> / 已完成 <b id=\"stat-finished\">0</b>
    （通过 <span class=\"badge passed\" id=\"stat-passed\">0</span>
     失败 <span class=\"badge failed\" id=\"stat-failed\">0</span>
     异常 <span class=\"badge broken\" id=\"stat-broken\">0</span>）
    | 进行中 <span class=\"badge running\" id=\"stat-running\">0</span>
    | 未完成 <b id=\"stat-unfinished\">0</b>
  </div>
  <div class=\"row\"><a href=\"/logs/stream\" target=\"_blank\">实时日志</a> | <a href=\"/output/html_report/index.html\" target=\"_blank\">Allure 报告</a></div>
  <h4>用例列表</h4>
  <ul id=\"list\"></ul>
</body>
</html>
    """
